/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */

import { assertWithMessage } from './Common.js';
import { FinalNodeId, NodeId, OpSpaceNodeId, SessionId, StableNodeId } from './Identifiers.js';
import { IdCompressor, isFinalId } from './id-compressor/index.js';
import { NodeData } from './persisted-types/index.js';

/**
 * An object which can generate node IDs and convert node IDs between compressed and stable variants
 * @alpha
 */
export interface NodeIdContext extends NodeIdGenerator, NodeIdConverter {}

/**
 * An object which can generate node IDs
 * @alpha
 */
export interface NodeIdGenerator {
	/**
	 * Generates a node identifier.
	 * The returned IDs may be used as the identifier of a node in the SharedTree.
	 * `NodeId`s are *always* unique and stable within the scope of the tree and session that generated them. They are *not* unique within
	 * a Fluid container, and *cannot* be compared across instances of a SharedTree. They are *not* stable across sessions/lifetimes of a
	 * SharedTree, and *cannot* be persisted (e.g. stored in payloads, uploaded in blobs, etc.). If stable persistence is needed,
	 * NodeIdConverter.convertToStableNodeId may be used to return a corresponding UUID that is globally unique and stable.
	 * @param override - if supplied, calls to `convertToStableNodeId` using the returned node ID will return the override instead of
	 * the UUID. Calls to `generateNodeId` with the same override always return the same ID. Performance note: passing an override string
	 * incurs a storage cost that is significantly higher that a node ID without one, and should be avoided if possible.
	 */
	generateNodeId(override?: string): NodeId;
}

/**
 * An object which can convert node IDs between compressed and stable variants
 * @alpha
 */
export interface NodeIdConverter {
	/**
	 * Given a NodeId, returns the corresponding stable ID or throws if the supplied node ID was not generated with this tree (`NodeId`s
	 * may not be used across SharedTree instances, see `generateNodeId` for more).
	 * The returned value will be a UUID, unless the creation of `id` used an override string (see `generateNodeId` for more).
	 * The result is safe to persist and re-use across `SharedTree` instances, unlike `NodeId`.
	 */
	convertToStableNodeId(id: NodeId): StableNodeId;

	/**
	 * Given a NodeId, attempt to return the corresponding stable ID.
	 * The returned value will be a UUID, unless the creation of `id` used an override string (see `generateNodeId` for more).
	 * The returned stable ID is undefined if `id` was never created with this SharedTree. If a stable ID is returned, this does not imply
	 * that there is a node with `id` in the current revision of the tree, only that `id` was at some point generated by some instance of
	 * this tree.
	 */
	tryConvertToStableNodeId(id: NodeId): StableNodeId | undefined;

	/**
	 * Given a stable ID, return the corresponding NodeId or throws if the supplied stable ID was never generated with this tree, either
	 * as a UUID corresponding to a `NodeId` or as an override passed to `generateNodeId`.
	 * If a stable ID is returned, this does not imply that there is a node with `id` in the current revision of the tree, only that
	 * `id` was at some point generated by an instance of this SharedTree.
	 */
	convertToNodeId(id: StableNodeId): NodeId;

	/**
	 * Given a stable ID, return the corresponding NodeId or return undefined if the supplied stable ID was never generated with this tree,
	 * either as a UUID corresponding to a `NodeId` or as an override passed to `generateNodeId`.
	 * If a stable ID is returned, this does not imply that there is a node with `id` in the current revision of the tree, only that
	 * `id` was at some point generated by an instance of this SharedTree.
	 */
	tryConvertToNodeId(id: StableNodeId): NodeId | undefined;
}

/**
 * An object which can normalize node IDs. See docs on {@link IdCompressor} for semantics of normalization.
 */
export interface NodeIdNormalizer<TId extends OpSpaceNodeId> {
	localSessionId: SessionId;
	/**
	 * Normalizes the given ID to op space
	 */
	normalizeToOpSpace(id: NodeId): TId;
	/**
	 * Normalizes the given ID to session space
	 */
	normalizeToSessionSpace(id: TId, sessionId: SessionId): NodeId;
}

/**
 * An object which can normalize node IDs. It is contextualized to a known session context, and therefore
 * can normalize IDs into session space without requiring any additional information.
 */
export interface ContextualizedNodeIdNormalizer<TId extends OpSpaceNodeId>
	extends Omit<NodeIdNormalizer<TId>, 'localSessionId' | 'normalizeToSessionSpace'> {
	/**
	 * Normalizes the given ID to session space
	 */
	normalizeToSessionSpace(id: TId): NodeId;
}

/**
 * Create a {@link ContextualizedNodeIdNormalizer} that uses either the given session ID to normalize IDs
 * to session space. If no ID is given, it will use the local session ID belonging to the normalizer.
 */
export function scopeIdNormalizer<TId extends OpSpaceNodeId>(
	idNormalizer: NodeIdNormalizer<TId>,
	sessionId?: SessionId
): ContextualizedNodeIdNormalizer<TId> {
	return {
		normalizeToOpSpace: (id) => idNormalizer.normalizeToOpSpace(id),
		normalizeToSessionSpace: (id) => idNormalizer.normalizeToSessionSpace(id, sessionId ?? idNormalizer.localSessionId),
	};
}

/**
 * Create a {@link ContextualizedNodeIdNormalizer} that uses the local session ID belonging to the normalizer
 * to normalize IDs to session space. These IDs are expected to be sequenced, and will fail to normalize if
 * they are not.
 */
export function sequencedIdNormalizer<TId extends OpSpaceNodeId>(
	idNormalizer: NodeIdNormalizer<TId>
): ContextualizedNodeIdNormalizer<FinalNodeId & TId> {
	return {
		normalizeToOpSpace: (id) => {
			const normalized = idNormalizer.normalizeToOpSpace(id);
			assertWithMessage(isFinalId(normalized));
			return normalized;
		},
		normalizeToSessionSpace: (id) => {
			const normalized = idNormalizer.normalizeToSessionSpace(id, idNormalizer.localSessionId);
			assertWithMessage(isFinalId(normalized));
			return normalized;
		},
	};
}

export function getNodeIdContext(compressor: IdCompressor): NodeIdContext & NodeIdNormalizer<OpSpaceNodeId> {
	return {
		generateNodeId: (override?: string) => compressor.generateCompressedId(override) as NodeId,
		convertToNodeId: (id: StableNodeId) => compressor.recompress(id) as NodeId,
		tryConvertToNodeId: (id: StableNodeId) => compressor.tryRecompress(id) as NodeId | undefined,
		convertToStableNodeId: (id: NodeId) => compressor.decompress(id) as StableNodeId,
		tryConvertToStableNodeId: (id: NodeId) => compressor.tryDecompress(id) as StableNodeId,
		normalizeToOpSpace: (id: NodeId) => compressor.normalizeToOpSpace(id) as OpSpaceNodeId,
		normalizeToSessionSpace: (id: OpSpaceNodeId, sessionId: SessionId) =>
			compressor.normalizeToSessionSpace(id, sessionId) as NodeId,
		localSessionId: compressor.localSessionId,
	};
}

/** Accepts either a node or a node's identifier, and returns the identifier */
export function getNodeId<TId extends number | string>(node: TId | NodeData<TId>): TId {
	return (node as NodeData<TId>).identifier ?? (node as TId);
}
